package edu.rpi.tw.impav;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import processing.core.PGraphics;
import traer.animation.Smoother3D;
import traer.physics.ParticleSystem;

import com.hp.hpl.jena.ontology.Individual;

/**
 * This class is a customized Java-fied version of the demo at:
 * http://www.cricketschirping.com/processing/GraphLayout2/
 * 
 * @author jim
 * 
 */
public class Graph extends processing.core.PShape {

    Smoother3D centroid = new Smoother3D(0.75f);

    private Map<Individual, Node> nodeMap = new HashMap<Individual, Node>();

    private Map<Node, Map<Node, Edge>> edgeMap = new HashMap<Node, Map<Node, Edge>>();

    Set<Node> nodes = new HashSet<Node>();
    Set<Edge> edges = new HashSet<Edge>();
    ParticleSystem ps = new ParticleSystem(0f, 0.5f);

    Node selectedNode;
    Node dragNode;
    Node hoverNode;

    public static float SPRING_CONSTANT = 0.5f;
    public static float DAMPING_CONSTANT = 0.00001f;
    public static int RESTLENGTH = 10;

    int MAGNET_CONSTANT = -2000;
    int MAGNET_DISTANCE = 2;

    int GRAVITY_CONSTANT = 4000;
    int GRAVITY_DISTANCE = 200;

    // In Milliseconds.
    long lifetime = 30000;
    
    float minTfidf = 0;

    private float positionSeedY;

    private float positionSeedX;

    private List<Tweet> allTweets = new LinkedList<Tweet>();

    public List<Tweet> getAllTweets() {
        return allTweets;
    }

    public long getTotalTweetCount() {
        return totalTweetCount;
    }

    private long totalTweetCount = 0;

    public long getCurrentTweetCount() {
        return currentTweetCount;
    }

    private long currentTweetCount = 0;

    private double xMax;

    private double xMin;

    private double yMin;

    private double yMax;

    public Graph(App application) {
        centroid.setValue(0.0f, 0.0f, 1.0f);
        centroid.setSmoothness(0.5f);
    }

    void updateCentroid() {

        xMax = Float.NEGATIVE_INFINITY;
        xMin = Float.POSITIVE_INFINITY;
        yMin = Float.POSITIVE_INFINITY;
        yMax = Float.NEGATIVE_INFINITY;

        for (Node n : nodes) {
            xMax = Math.max(xMax, n.position().x() + n.getRadius());
            xMin = Math.min(xMin, n.position().x() - n.getRadius());
            yMin = Math.min(yMin, n.position().y() - n.getRadius());
            yMax = Math.max(yMax, n.position().y() + n.getRadius());
        }

        if (ps.numberOfParticles() == 0)
            return;

        double deltaX = xMax - xMin;
        double deltaY = yMax - yMin;
        if (deltaY > deltaX)
            centroid.setTarget((float) (xMin + 0.5 * deltaX),
                    (float) (yMin + 0.5 * deltaY),
                    (float) (height / (deltaY + 1)));
        else
            centroid.setTarget((float) (xMin + 0.5 * deltaX),
                    (float) (yMin + 0.5 * deltaY),
                    (float) (width / (deltaX + 1)));
    }

    public Smoother3D getCentroid() {
        return centroid;
    }

    public synchronized void removeNode(Node n) {
        n.remove();
        if (selectedNode == n)
            selectedNode = null;
        if (hoverNode == n)
            hoverNode = null;
        if (dragNode == n)
            dragNode = null;

        HashSet<Edge> toRemove = new HashSet<Edge>();
        for (Edge e : edges) {
            if (e.a == n || e.b == n)
                toRemove.add(e);
        }
        edges.removeAll(toRemove);
    }

    public void seedPosition(List<Individual> vector) {
        double radius = 10;
        if (yMax != Float.NEGATIVE_INFINITY && yMin != Float.POSITIVE_INFINITY)
            radius = 1.1* (yMax - yMin)/2;
        double angle = Math.random()*Math.PI*2;
        positionSeedX = (float) (Math.cos(angle)*radius);
        positionSeedY = (float) (Math.sin(angle)*radius);
    }
    
    public synchronized Node getNode(Individual i) {
        if (!getNodeMap().containsKey(i)) {
            Node n = new Node(i, this);
            ps.addParticle(n);
            getNodeMap().put(n.getConcept(), n);
            getEdgeMap().put(n, new HashMap<Node, Edge>());
        }
        Node n = getNodeMap().get(i);
        ps.addParticle(n);
        synchronized (nodes) {
            if (!nodes.contains(n)) {
                n.position().set((float) (positionSeedX + Math.random()),
                        (float) (positionSeedY + Math.random()), 0.0f);
                n.buildAttractions();
                nodes.add(n);
            }
        }
        return n;
    }

    public Edge getEdge(Node from, Node to) {
        if (!getEdgeMap().containsKey(from))
            getEdgeMap().put(from, new HashMap<Node, Edge>());
        if (!getEdgeMap().containsKey(to))
            getEdgeMap().put(to, new HashMap<Node, Edge>());
        if (!getEdgeMap().get(from).containsKey(to)) {
            Edge e = newEdge(from, to);
            getEdgeMap().get(from).put(to, e);
            getEdgeMap().get(to).put(from, e);
        }
        Edge e = getEdgeMap().get(from).get(to);
        ps.addSpring(e);
        if (!edges.contains(e))
            edges.add(e);
        return e;
    }

    public synchronized Edge newEdge(Node from, Node to) {
        Edge e = new Edge(from, to, this);
        edges.add(e);
        return e;
    }

    public void tick(PGraphics g) {
        
        synchronized(nodes) {
            for (Node n : nodes) {
                n.update(g);
                n.setVisible(n.getTfIdf() >= minTfidf);
            }
            
            for (Edge e : edges) {
                e.update(g);
            }

            getPhysics().tick( 0.5f );

            updateCentroid();
            getCentroid().tick();
        }

        
    }
    public void draw(PGraphics graphics) {
        synchronized(nodes) {
            for (Edge e : edges) {
                e.draw(graphics);
            }
            List<Node> sortedNodes = new ArrayList<Node>();
            sortedNodes.addAll(nodes);
            Collections.sort(sortedNodes);
            for (Node n : sortedNodes) {
                n.draw(graphics);
            }
        }
    }

    public void runExpiration() {
        Date now = new Date();
        synchronized (nodes) {
            for (Node n : new LinkedList<Node>(nodes)) {
                synchronized (n) {
                    synchronized(n.getTweets()) {
                        expireTweets(n.getTweets(), now);
                        if (n.getTweets().size() == 0)
                            this.removeNode(n);
                    }
                }
            }
        }
        for (Edge e : new LinkedList<Edge>(edges)) {
            expireTweets(e.getTweets(), now);
            if (e.getTweets().size() == 0) {
                edges.remove(e);
                e.remove();
            }
        }
        expireTweets(allTweets, now);
        currentTweetCount = allTweets.size();
    }

    private void expireTweets(List<Tweet> tweets, Date now) {
        Set<Tweet> toRemove = new HashSet<Tweet>();
        synchronized(tweets) {
            for (Tweet t : tweets) {
                if (t.created.getTime() + lifetime <= now.getTime())
                    toRemove.add(t);
            }
            tweets.removeAll(toRemove);
        }
    }

    public void addTweet(Tweet t) {
        allTweets.add(0,t);
        totalTweetCount++;
        currentTweetCount++;
    }

    public Set<Node> getNodes() {
        return nodes;
    }

    public void setSelectedNode(Node n) {
        if (selectedNode != null) {
            selectedNode.setSelected(false);
        }

        selectedNode = n;

        if (n != null)
            n.setSelected(true);
    }

    public Node getSelectedNode() {
        return selectedNode;
    }

    public void setHoverNode(Node n) {
        if (hoverNode != null) {
            hoverNode.setHovering(false);
        }

        hoverNode = n;
        if (n != null)
            n.setHovering(true);
    }

    public Node getHoverNode() {
        return hoverNode;
    }

    public void setDragNode(Node n) {
        if (dragNode != null) {
            dragNode.setDragging(false);
        }

        dragNode = n;

        if (n != null)
            n.setDragging(true);
    }

    public Node getDragNode() {
        return dragNode;
    }

    public ParticleSystem getPhysics() {
        return ps;
    }

    public Map<Individual, Node> getNodeMap() {
        return nodeMap;
    }

    public Map<Node, Map<Node, Edge>> getEdgeMap() {
        return edgeMap;
    }

    public void setMinTfidf(float minTfidf) {
        this.minTfidf = minTfidf;
    }

}
